Skip to content

fix(ci): align the live-e2e backend pin with the lockfile, and pin the rule that says it must - #7970

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-7689-backend-env-pin-matches-lockfile
Sep 6, 2026
Merged

fix(ci): align the live-e2e backend pin with the lockfile, and pin the rule that says it must#7970
baozhoutao merged 2 commits into
mainfrom
claude/issue-7689-backend-env-pin-matches-lockfile

Conversation

@baozhoutao

@baozhoutao baozhoutao commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7689

e2e/live/ci/backend.env pinned OBJECTSTACK_VERSION=17.0.0-rc.2 while pnpm-lock.yaml resolved @objectstack/spec 17.2.0. Three separate places state that the two MUST match and that a mismatched pair “proves nothing”: the file's own header, content/docs/guide/ci-cd-pipeline.md:420, and .github/workflows/live-e2e.yml's header at :27. For two minor versions Live E2E (informational) therefore published a green signal its own contract declares meaningless — because nothing compared the two numbers.

The pair pinned, and why these values

key was now why
OBJECTSTACK_VERSION 17.0.0-rc.2 17.2.0 the version the lockfile resolves, not the ^17.0.0 range it declares. Verified on this branch's base: @objectstack/spec@ appears in pnpm-lock.yaml at exactly one version, in both spellings (the bare packages: key and the peer-suffixed snapshots: key), and all 10 workspace importers resolve 17.2.0.
OBJECTSTACK_REF 89d2a4eb3f3b… e7d2cc67fdef… the file header's own pairing rule: the commit the @objectstack/cli@ + that version release tag points at. Measured, not guessed — @objectstack/cli@17.2.0 dereferences to e7d2cc67fdef7fee9d2c6d65d7363fe1c78ce6a4. The rule was being honoured before: the outgoing ref is byte-for-byte the commit @objectstack/cli@17.0.0-rc.2 points at.

Registry probe before pinning: every @objectstack/* package start-backend.sh installs — the nine runtime deps the showcase app declares at that ref, plus @objectstack/cli — publishes 17.2.0. None had to be substituted, so this is not a needs_decision.

Deliberately not 17.3.0: that is PR #7685's lockfile, not this branch's.

The check

scripts/__tests__/ci-cd-pipeline-doc.test.ts, in a new describe. That file's charter is pinning this exact page's claims to CI reality, and the sentence at ci-cd-pipeline.md:420 is a claim of exactly the shape it was written for — its header already says a doc advertising a guardrail CI does not have is worse than no doc.

The version-claim ledger doc-version-claims.test.ts was the dispatch's first candidate and was ruled out on measurement, not taste: its SCAN_ROOTS are content/docs, packages/*/README.md and skills, .md/.mdx only, so backend.env is not on a scanned surface and no KNOWN_CLAIMS row holds the 17.0.0-rc.2 literal. Its own rule — one rule judges one kind of fact — argues against bolting a two-non-doc-file comparison onto it.

Four assertions: the doc sentence is still on the page (this whole block is its enforcement); the lockfile resolves exactly one @objectstack/spec version, read two independent ways whose union must be a singleton; OBJECTSTACK_VERSION equals it; and OBJECTSTACK_REF is a full 40-character object name, because git fetch --depth 1 refuses an abbreviated one and the lane only says so 300 seconds later.

Both anti-vacuity floors this file's culture asks for are in: an empty lockfile match set is a failure, not a silent pass, and a missing doc sentence is a failure rather than a quiet retirement.

content/docs/guide/ci-cd-pipeline.md is unchanged: the sentence names no version number, so it did not misstate anything after the pin moved.

Second commit, after review. The reader sweep for the outgoing literal (git grep -n '17\.0\.0-rc\.2' -- scripts/ content/ e2e/ .github/) returns exactly two hits, and the first is the pin itself; the second was e2e/live/saved-view-filter.spec.ts:10, whose header restated the pair as a value and went stale the moment the pin moved. It now names OBJECTSTACK_VERSION / OBJECTSTACK_REF in e2e/live/ci/backend.env instead, and says the pair has moved since that measurement rather than implying it has not — comment only, with no assertion, import or code line touched (git diff -U0 on that file yields no changed line that is not a comment line).

The half deliberately not asserted, stated in both the env file and the test rather than implied as covered: whether OBJECTSTACK_REF really is the tag's commit. Reading that tag needs the objectstack repository over the network, which the unit lane has not got. Filed separately as #7964.

Ablation, on the committed tree

Three legs, each proving on disk that the mutation landed (injected/removed line counts and git hash-object) before reading the run, with a trap restoring from HEAD by absolute path:

leg mutation vitest what it proves
1 pin back to 17.0.0-rc.2 exit 1, 1 failed / 39 passed the check names both files, both values and the doc sentence — the assertion message reads e2e/live/ci/backend.env OBJECTSTACK_VERSION=17.0.0-rc.2 against pnpm-lock.yaml @objectstack/spec 17.2.0
2 abbreviate the ref to e7d2cc6 exit 1, 1 failed / 39 passed the shape assertion is not vacuous
3 restored exit 0, 40 passed restoration verified by blob equality (db35d5ef… back to the HEAD blob) and an empty git diff HEAD, not by an exit code

No dist leg: the test reads backend.env and pnpm-lock.yaml straight off the working tree, so there is no build output for a mutation to fail to reach.

The live lane will go red here, and that is the point

Per the triage ruling on the card — 「一条失败的匹配对携带的信息,严格多于一条绿色的不匹配对」 — a red Live E2E (informational) after alignment is the expected result, not a regression, and ⛔ the repair is never to put the pin back.

Measured in this container before opening the PR, so it is no longer a prediction. The matched pair prepares cleanly — sparse checkout at the new ref, npm install of the ten published @objectstack/*@17.2.0 packages, 521 packages — and the server starts, but never seeds: the system tables are never created (no such table: sys_user, sys_organization, sys_permission_set, sys_position), so the seeded sign-in start-backend.sh polls never answers and the script exits 1 at its 300-second deadline.

The cause is upstream of this repository and was traced to a single declaration, from the registry rather than by inference:

published @objectstack/plugin-auth declares @better-auth/core resolves to boots
17.0.0-rc.2 (the outgoing pin) 1.7.0-rc.2exact 1.7.0-rc.2 yes
17.1.0 ^1.7.1 1.7.3 no
17.2.0 (the incoming pin) ^1.7.1 1.7.3 no
17.3.0 (PR #7685's target) ^1.7.2 1.7.3 expected no

@objectstack/plugin-auth@17.2.0 imports createLocalAccountIssuer from @better-auth/core/db; @better-auth/core@1.7.3 does not export it (verified by grep over the installed package: zero hits), and the CLI logs exactly that as a load failure before the schema step. So the outgoing pin booted only because rc.2 happened to pin its auth dependency exactly, and every published version since has floated into a range that is now broken.

A control leg run minutes later in the same container, through the same start-backend.sh, boots the outgoing pair (@objectstack/*@17.0.0-rc.2 at ref 89d2a4eb…) to a seeded sign-in in 30 seconds, with zero createLocalAccountIssuer lines and zero no such table lines. That is the load-bearing half of the measurement: it rules out "this container cannot boot the lane at all" and attributes the failure to the published version rather than to the harness or the alignment.

That is precisely the class of breakage this lane exists to catch, and precisely what an unmatched pair could not tell anyone. It is not a regression introduced here and ⛔ must not be repaired by reverting the pin. Filed upstream, where the change belongs, as objectstack-ai/objectstack#16186.

⛔ Out of scope on purpose, per the triage: the lane's tier. informational stays; promoting it is the manual floor and its own decision card. No change to .github/workflows/live-e2e.yml, pnpm-lock.yaml or any package.json.

For the domain:spec seat merging main

PR #7685 (the 17.3.0 lockfile bump) does not move backend.env; this check will red there until it does.

Verification

  • pnpm exec vitest run --maxWorkers=2 scripts/__tests__/ci-cd-pipeline-doc.test.ts — 40 passed (36 before, 4 added)
  • pnpm exec vitest run --maxWorkers=2 scripts/__tests__/{doc-version-claims,check-control-bytes,e2e-type-check,scripts-type-check}.test.ts — 94 passed
  • pnpm type-check:scripts — exit 0; tsc --listFiles confirms scripts/__tests__/ci-cd-pipeline-doc.test.ts is in the program, so this is a measurement and not an exclusion
  • pnpm lint:root — exit 0, 0 errors (32 pre-existing warnings, none in a changed file). Full repo-root scan, not narrowed
  • pnpm check:control-bytes — OK, 6415 files; pnpm check:shell-escape-residue — OK
  • node scripts/check-governed-queue-guard.mjs --test on both changed paths — NOT GOVERNED
  • node scripts/check-changeset-presence.mjs — no changeset owed (0 published sources, 0 published contracts changed)
  • pnpm type-check:e2e (tsconfig.e2e.json, the program that owns that spec) — exit 0; --listFiles puts e2e/live/saved-view-filter.spec.ts in the program (1 hit), so it is measured and not excluded
  • pnpm exec eslint --format json on the changed spec — exit 0, 0 errors, 0 warnings, 1 file reported (a count of 0 would have meant out-of-scope, which is not a pass); the whole e2e/**/*.ts glob lints at 0 errors across 31 files
  • The report's claim that the version-claim ledger cannot see e2e/ is now measured, not asserted: with the literal injected into that spec, doc-version-claims.test.ts stays green (exit 0), and with the same literal on a throwaway page under content/docs/ it goes red (exit 1). The negative reading is a blind spot, not a dead instrument. Both probes restored; tree verified clean afterwards.

Heavy runs went through the shared verify lock; every verdict above was read from the gate's own printed line with the exit code captured before any pipe.

Authored by a Claude Code developer seat, session session_013uAaxiwgYDybsTNV9xwa1M (repeated here as prose because a footer link does not reliably survive an edit to this body).

Judgement, on the four axes

  • Real business need — measured, not assumed: start-backend.sh sources this file at :25, builds its cache stamp from both keys at :32 and installs from OBJECTSTACK_VERSION at :41 and :65; live-e2e.yml keys the backend cache on the file's hash at :133 and :302. Two live readers, and the lane they serve is the only one that can see cross-version breakage at all.
  • Long-term soundness — contract-first. The rule was already declared in prose in three places; this adds the enforcement the declarations always implied, in a file that already exists for it. No new convention, no lenient fallback, nothing to unwind later.
  • Making it structurally harder to get wrong — the check refuses rather than tolerates. A lockfile resolving two @objectstack/spec versions is named as a split, not silently reduced to one to compare against; an empty match set fails instead of passing; and the failure text forbids by name the repair that would restore green while restoring meaninglessness.
  • Startup-stage focus — one describe in an existing file. No new script, no new workflow, no new declaration surface, and the two expansions available here were both declined: the tier change (the card's own out-of-scope half) and a network check for the ref (a real fork between checking the pin and deriving it, filed as finding(ci): the live-e2e lane's OTHER pin — OBJECTSTACK_REF — states a MUST that nothing can check, and it fails silently in the same way objectui#7689 did #7964 for the maintainer rather than picked here).

Generated by Claude Code

`e2e/live/ci/backend.env` pinned `OBJECTSTACK_VERSION=17.0.0-rc.2` while
`pnpm-lock.yaml` resolved `@objectstack/spec` 17.2.0. Both the file's own
header and `content/docs/guide/ci-cd-pipeline.md` state the two must match,
and that a mismatched pair "proves nothing" -- so for two minor versions the
`Live E2E (informational)` lane produced a green signal its own contract
declares meaningless.

Aligns the pair to the value the lockfile actually resolves (17.2.0, not the
`^17.0.0` range) and moves `OBJECTSTACK_REF` with it, to the commit the
`@objectstack/cli@17.2.0` release tag points at -- the pairing rule the file
header already states, and the one the previous pin honoured for rc.2.

Adds the missing comparison to `scripts/__tests__/ci-cd-pipeline-doc.test.ts`,
whose charter is pinning that page's claims to CI reality: it reads
`OBJECTSTACK_VERSION`, reads every `@objectstack/spec` version the lockfile
resolves (two independent readings, union must be one), and requires equality,
with both anti-vacuity floors the file's culture asks for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M
…eral

`e2e/live/saved-view-filter.spec.ts`'s header stated the measurement was taken
on `@objectstack/*@17.0.0-rc.2` -- the pair the previous commit just moved off.
It was the second reader of that literal on the fenced surfaces, and it went
stale the moment the pin did.

Names `OBJECTSTACK_VERSION` / `OBJECTSTACK_REF` in `e2e/live/ci/backend.env`
instead, and says the pair has moved since the measurement rather than implying
it has not. A comment that carries the value is a copy of the pin that nothing
updates; a comment that carries the file name cannot go stale behind it.

Comment only -- no assertion, import or code line is touched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013uAaxiwgYDybsTNV9xwa1M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants